package com.trip.wechat.platform.service.impl;


import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Strings;
import com.trip.wechat.platform.dao.UserDao;
import com.trip.wechat.platform.datastore.MpAccountStore;
import com.trip.wechat.platform.domain.UserPO;
import com.trip.wechat.platform.model.AccountBO;
import com.trip.wechat.platform.model.RedisKey;
import com.trip.wechat.platform.model.user.UserBO;
import com.trip.wechat.platform.model.user.UserInfoBO;
import com.trip.wechat.platform.model.user.UserList;
import com.trip.wechat.platform.model.user.UserListResultBO;
import com.trip.wechat.platform.service.TokenService;
import com.trip.wechat.platform.service.UserService;
import com.trip.wechat.platform.subsystem.UserInfoFacade;
import org.apache.commons.lang3.StringEscapeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.List;
import java.util.Optional;

/**
 * 功能说明: <br>
 * 系统说明: <br>
 * 模块说明: <br>
 * 功能描述: <br>
 * <br>
 */
@Service
public class UserServiceImpl implements UserService {

    final Logger LOGGER = LoggerFactory.getLogger(this.getClass());

    @Autowired
    MpAccountStore accountStore;
    @Autowired
    TokenService tokenService;
    @Autowired
    UserInfoFacade userInfoFacade;
    @Autowired
    UserDao userDao;

    @Autowired
    StringRedisTemplate redisTemplate;

    ObjectMapper objectMapper = new ObjectMapper();

    volatile boolean running = false;

    @Override
    @Async
    public void fetchAndSync(String publicKey) {
        if (Strings.isNullOrEmpty(publicKey)) {
            LOGGER.info("[UserService.push] failed! publicKey cannot be empty!");
            return;
        }

        Optional<AccountBO> op = accountStore.get(publicKey);
        if (!op.isPresent()) {
            LOGGER.info("[UserService.push] failed! unknown publicKey:{}", publicKey);
            return;
        }

        String accessToken = tokenService.token(publicKey);
        if (Strings.isNullOrEmpty(accessToken)) {
            LOGGER.info("[UserService.push] failed! fetch token failed!");
            return;
        }

        UserListResultBO resultBO = doFetch(publicKey, null);
        doSync(publicKey, resultBO);
        while (resultBO.getErrcode() == 0
                && resultBO.getCount() > 0 && resultBO.getCount() < resultBO.getTotal()) {
            resultBO = doFetch(publicKey, resultBO.getNextId());
            doSync(publicKey, resultBO);
        }
    }

    @Override
    public void submitTask(UserList userList) {
        if (userList == null
                || Strings.isNullOrEmpty(userList.getPublicKey())
                || userList.getOpenid() == null
                || userList.getOpenid().size() == 0) {
            return;
        }

        try {
            String s = objectMapper.writeValueAsString(userList);
            redisTemplate.boundListOps(RedisKey.USER_TASK_QUEUE).leftPush(s);
        } catch (Exception e) {
            LOGGER.warn("[UserService.submitTask] failed!", e);
        }
    }

    @Override
    public void syncUserInfo(UserBO user) {
        if (user == null
                || Strings.isNullOrEmpty(user.getOpenid())
                || Strings.isNullOrEmpty(user.getPublicKey())) {
            return;
        }

        try {
            UserPO po = new UserPO();
            BeanUtils.copyProperties(user, po);
            if (!Strings.isNullOrEmpty(user.getNickname())) {
                po.setNickname(StringEscapeUtils.escapeJava(user.getNickname()));
            }
            LOGGER.info("[UserService.sync] openid:{}, publickey:{}, subscribe:{}",
                    po.getOpenid(), po.getPublicKey(), po.getSubscribe());
            userDao.merge(po);
        } catch (Exception e) {
            LOGGER.warn("[UserService.syncUserInfo] failed!", e);
        }
    }

    @Override
    @Scheduled(fixedRateString = "${wx.platform.users.task.scanner.rate: 5000}")
    public void scan() {
        if (running) {
            return;
        }
        running = true;
        try {
            String s = null;
            while ((s = redisTemplate.boundListOps(RedisKey.USER_TASK_QUEUE).rightPop()) != null) {
                UserList userList = objectMapper.readValue(s, UserList.class);
                if (userList != null
                        && !Strings.isNullOrEmpty(userList.getPublicKey())
                        && userList.getOpenid() != null
                        && userList.getOpenid().size() > 0) {
                    userList.getOpenid()
                            .stream()
                            .forEach(openid -> {
                                try {
                                    UserInfoBO info = doFetchInfo(userList.getPublicKey(), openid);
                                    if (info.getErrcode() == 0 && !Strings.isNullOrEmpty(info.getOpenid())) {
                                        UserBO userBO = new UserBO();
                                        BeanUtils.copyProperties(info, userBO);

                                        userBO.setPublicKey(userList.getPublicKey());

                                        syncUserInfo(userBO);
                                    } else {
                                        LOGGER.info("[UserService.doFetchInfo] failed! info => {}", info);
                                    }
                                } catch (Exception e) {
                                    LOGGER.info("[UserService.doFetchInfo] failed! openid=>{}", openid, e);
                                }
                            });
                }
            }
        } catch (Exception e) {
            LOGGER.warn("[UserService.scan] failed!", e);
        } finally {
            running = false;
        }
    }

    @Override
    public UserBO queryUserInfoByOpenidFromDB(String publicKey, String openid) {
        if(StringUtils.isEmpty(publicKey) || StringUtils.isEmpty(openid)){
//            throw new ParameterException("缺少publicKey/openid信息");
        }
        Optional<UserPO> optional = userDao.selectUserInfoByOpenid(publicKey, openid);

        if(optional.isPresent()){
            UserBO userBO = new UserBO();
            BeanUtils.copyProperties(optional.get(), userBO);
            userBO.setNickname(StringEscapeUtils.unescapeJava(userBO.getNickname()));
            return userBO;
        } else {
            LOGGER.info("[UserService.queryUserInfoByOpenidFromDB] not found openid[{}] 's info with publicKey[{}]"
                    , openid, publicKey);
            return null;
        }
    }

    protected UserInfoBO doFetchInfo(String publicKey, String openId) {
        String accessToken = tokenService.token(publicKey);
        if (Strings.isNullOrEmpty(accessToken)) {
            LOGGER.info("[UserService.doFetchInfo] failed! fetch token failed!");

            return createErrorResultForUserInfo(-1000, "token is empty");
        }

        UserInfoBO resultBO = userInfoFacade.fetchUserInfo(accessToken, openId);
        if (resultBO.getErrcode() == 42001) {   // token is expired
            accessToken = tokenService.refreshToken(publicKey);
            if (Strings.isNullOrEmpty(accessToken)) {
                LOGGER.info("[UserService.doFetchInfo] failed! refresh token failed!");
                return createErrorResultForUserInfo(-1000, "token is empty");
            }
            return userInfoFacade.fetchUserInfo(accessToken, openId);
        }
        return resultBO;
    }

    protected UserListResultBO doFetch(String publicKey, String nextOpenId) {
        String accessToken = tokenService.token(publicKey);
        if (Strings.isNullOrEmpty(accessToken)) {
            LOGGER.info("[UserService.doFetch] failed! fetch token failed!");

            return createErrorResult(-1000, "token is empty");
        }
        UserListResultBO resultBO = userInfoFacade.fetchUserList(accessToken, nextOpenId);
        if (resultBO.getErrcode() == 42001) {   // token is expired
            accessToken = tokenService.refreshToken(publicKey);
            if (Strings.isNullOrEmpty(accessToken)) {
                LOGGER.info("[UserService.doFetch] failed! refresh token failed!");
                return createErrorResult(-1000, "token is empty");
            }
            return userInfoFacade.fetchUserList(accessToken, nextOpenId);
        }
        return resultBO;
    }

    protected void doSync(String publicKey, UserListResultBO resultBO) {
        if (resultBO.getErrcode() == 0 && resultBO.getCount() > 0) {
            try {
                // split
                int window = 1000;
                int splitters = Math.min(resultBO.getData().getOpenid().size() / window + 1, 10);
                for (int i = 0; i < splitters; i ++) {
                    int start = i * window;
                    int endPos = (i+1) * window;
                    if (start >= resultBO.getData().getOpenid().size()) {
                        break;
                    }
                    // 最后一个window
                    if (i == splitters - 1) {
                        endPos = resultBO.getData().getOpenid().size();
                    }
                    List<String> subList = resultBO.getData().getOpenid().subList(start, endPos);

                    UserList subUserList = new UserList();
                    subUserList.setOpenid(subList);
                    subUserList.setPublicKey(publicKey);
                    submitTask(subUserList);
                }
            } catch (Exception e) {
                LOGGER.info("[UserService.doSync] failed!", e);
            }
        }
    }

    protected UserListResultBO createErrorResult(int code, String message) {
        UserListResultBO result = new UserListResultBO();
        result.setErrcode(code);
        result.setErrmsg(message);
        return result;
    }

    protected UserInfoBO createErrorResultForUserInfo(int code, String message) {
        UserInfoBO result = new UserInfoBO();
        result.setErrcode(code);
        result.setErrmsg(message);
        return result;
    }
}
